using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Globalization;
using System.Collections.ObjectModel;

namespace TelnetExpect
{
    /// <summary>
    /// Provides Telnet communications and is quite capable regarding negotiation.
    /// </summary>
    /// -
    /// <remarks>
    /// <para>Defaults to agreeing to <see cref="F:TelnetExpect.TelnetOption.BinaryTransmission"/>
    /// in both directions, and to
    /// the server enabling <see cref="F:TelnetExpect.TelnetOption.Echo"/> and
    /// <see cref="F:TelnetExpect.TelnetOption.SuppressGoAhead"/>.  If being used in
    /// scripted scenario to stop your sent keypresses/commands being returned
    /// in the received data then disable Echo (<c>ts.SetRemoteMode(TelnetOption.Echo, false)</c>).
    /// </para>
    /// <para>Similar to <see cref="T:TelnetExpect.TelnetBaseStream"/> the negotiation
    /// processing is driven by the <see cref="M:System.IO.Stream.Read"/> method
    /// being called.
    /// </para>
    /// </remarks>
    public class TelnetStream : TelnetBaseStream
    {
        /// <summary>
        /// Initialize an instance of the <see cref="T:TelnetStream"/> class.
        /// </summary>
        /// <param name="peer"></param>
        public TelnetStream(Stream peer)
            : base(peer)
        {
            SetDefaultModes();
        }

        protected virtual void SetDefaultModes()
        {
            SetRemoteMode(TelnetOption.Echo, true);
            SetRemoteMode(TelnetOption.SuppressGoAhead, true);
            SetLocalMode(TelnetOption.BinaryTransmission, true);
            SetRemoteMode(TelnetOption.BinaryTransmission, true);
        }

        //--------
        protected const int StateSize = 256;
        protected bool[] m_stateRemote = new bool[StateSize];
        protected bool[] m_canRemote = new bool[StateSize];
        //
        protected bool[] m_stateLocal = new bool[StateSize];
        protected bool[] m_canLocal = new bool[StateSize];
        //
        List<TelnetOptionHandler> m_optionHandlerChain = new List<TelnetOptionHandler>();

        //--------
        /// <summary>
        /// Get whether we set that
        /// we can support the option as it applies to the
        /// remote side.
        /// </summary>
        /// -
        /// <returns>A <see cref="T:System.Boolean"/> indicating whether the option is set as supported.
        /// </returns>
        public bool GetRemoteMode(TelnetOption option)
        {
            bool enable = m_canRemote[(int)option];
            return enable;
        }

        /// <summary>
        /// Set whether we can support the option as it applies to the remote side.
        /// </summary>
        /// -
        /// <param name="option">A <see cref="TelnetOption"/> specifying the option
        /// id.
        /// </param>
        /// <param name="enable">A <see cref="T:System.Boolean"/> indicating
        /// whether the option is supported.
        /// </param>
        public void SetRemoteMode(TelnetOption option, bool enable)
        {
            m_canRemote[(int)option] = enable;
        }

        /// <summary>
        /// Get the state of the specified option as it applies to the
        /// remote side.
        /// </summary>
        /// -
        /// <param name="option">A <see cref="TelnetOption"/> specifying the option
        /// id.
        /// </param>
        /// -
        /// <returns>A <see cref="T:System.Boolean"/> indicating whether the option is currently enabled.
        /// </returns>
        public bool GetRemoteState(TelnetOption option)
        {
            bool enable = m_stateRemote[(int)option];
            return enable;
        }

        protected void SetRemoteState(TelnetOption option, bool enable)
        {
            m_stateRemote[(int)option] = enable;
        }

        //--------
        /// <summary>
        /// Get whether we set that
        /// we can support the option as it applies to the
        /// local side.
        /// </summary>
        /// -
        /// <returns>A <see cref="T:System.Boolean"/> indicating whether the option is set as supported.
        /// </returns>
        public bool GetLocalMode(TelnetOption option)
        {
            bool enable = m_canLocal[(int)option];
            return enable;
        }

        /// <summary>
        /// Set whether
        /// we can support the option as it applies to the
        /// local side.
        /// </summary>
        /// -
        /// <param name="option">A <see cref="TelnetOption"/> specifying the option
        /// id.
        /// </param>
        /// <param name="enable">A <see cref="T:System.Boolean"/> indicating
        /// whether the option is supported.
        /// </param>
        public void SetLocalMode(TelnetOption option, bool enable)
        {
            m_canLocal[(int)option] = enable;
        }

        /// <summary>
        /// Get the state of the specified option as it applies to the 
        /// local side.
        /// </summary>
        /// -
        /// <param name="option">A <see cref="TelnetOption"/> specifying the option
        /// id.
        /// </param>
        /// -
        /// <returns>A <see cref="T:System.Boolean"/> indicating whether the option is currently enabled.
        /// </returns>
        public bool GetLocalState(TelnetOption option)
        {
            bool enable = m_stateLocal[(int)option];
            return enable;
        }

        protected void SetLocalState(TelnetOption option, bool enable)
        {
            m_stateLocal[(int)option] = enable;
        }

        //----
        /// <summary>
        /// Get whether the remote side is responsible for echoing typed characters.
        /// </summary>
        /// -
        /// <remarks>
        /// <para>Of course only applies when we are the client side of the connection.
        /// </para>
        /// </remarks>
        public bool IsRemoteEcho { get { return GetRemoteState(TelnetOption.Echo); } }

        //----
        protected override void HandleNegotiation(Command command, TelnetOption option)
        {
            Debug.Assert(IsNegotiationCommand(command), "Our base passed an invalid Negotiation Command: " + command);
            bool ability, prevState;
            bool newState;
            Command cmdOn, cmdOff;
            bool local;
            if (command == Command.Will || command == Command.Wont) {
                local = false;
                ability = GetRemoteMode(option);
                prevState = GetRemoteState(option);
                cmdOn = Command.Do;
                cmdOff = Command.Dont;
            } else {
                local = true;
                ability = GetLocalMode(option);
                prevState = GetLocalState(option);
                cmdOn = Command.Will;
                cmdOff = Command.Wont;
            }
            Debug.Assert(!prevState || ability, "State=Enabled but Abilty=False!!");
            //
            switch (command) {
                case Command.Will:
                case Command.Do:
                    if (ability) {
                        newState = true;
                    } else {
                        newState = false;
                    }
                    DoOnChangingState(local, option, newState, prevState, command);
                    // TODO If not disagreement from user.
                    if (newState && !prevState && ability) {
                        AddResponseCommand(cmdOn, option);
                    } else if (!ability) {
                        Debug.Assert(newState == false, "OFF!");
                        AddResponseCommand(cmdOff, option);
                    }
                    break;
                case Command.Wont:
                case Command.Dont:
                default:
                    newState = false; // Can only agree
                    if (prevState) {
                        AddResponseCommand(cmdOff, option);
                    }
                    break;
            }//switch
            //
            if (newState != prevState) {
                DoOnChangedState(local, option, newState, prevState, command);
                if (local) {
                    SetLocalState(option, newState);
                } else {
                    SetRemoteState(option, newState);
                }
            }
        }

        protected void DoOnChangingState(bool local, TelnetOption option, bool newState, bool already, Command command)
        {
        }

        protected void DoOnChangedState(bool local, TelnetOption option, bool newState, bool already, Command command)
        {
            foreach (TelnetOptionHandler cur in m_optionHandlerChain) {
                cur.DoOnChangedState(local, option, newState, already, command);
            }
        }

        //--------
        /// <summary>
        /// Add a <see cref="T:TelnetExpect.TelnetOptionHandler"/> which can handle
        /// negotiation or sub-negotiation.
        /// </summary>
        /// -
        /// <param name="handler">An instance of a sub-class of 
        /// <see cref="T:TelnetExpect.TelnetOptionHandler"/>.
        /// </param>
        public void AddOptionHandler(TelnetOptionHandler handler)
        {
            handler.SetParent(this);
            handler.Init();
            m_optionHandlerChain.Add(handler);
        }

        public ICollection<TelnetOptionHandler> GetOptionHandlers()
        {
            var c = new ReadOnlyCollection<TelnetOptionHandler>(m_optionHandlerChain);
            return c;
        }

        protected override void HandleSubNegotiation(byte[] data)
        {
            bool gotData = false;
            foreach (TelnetOptionHandler cur in m_optionHandlerChain) {
                gotData = cur.DoHandleSubNegotiation(data);
                if (gotData) {
                    break;
                }
            }
            if (!gotData) {
                string code = null;
                if (data.Length >= 1) {
                    TelnetOption optionCode = (TelnetOption)data[0];
                    code = " (" + optionCode.ToString() + ")";
                }
                TraceWriteLine("Discarding SubNegotiation" + code + ": ", data, 0, data.Length);
            }
        }

        //--------
        public static TelnetStream Connect(string hostName)
        {
            System.Net.Sockets.TcpClient cli = new System.Net.Sockets.TcpClient(hostName, DefaultPort);
            return Connect(cli);
        }

        public static TelnetStream Connect(System.Net.IPAddress address)
        {
            if (address == null)
                throw new ArgumentNullException("address");
            System.Net.Sockets.TcpClient cli = new System.Net.Sockets.TcpClient(address.AddressFamily);
            cli.Connect(address, DefaultPort);
            return Connect(cli);
        }

        private static TelnetStream Connect(System.Net.Sockets.TcpClient cli)
        {
            return new TelnetStream(cli.GetStream());
        }

    }
}
